package com.google.zxing.journeyapps.barcodescanner;

import com.google.zxing.ResourceTable;
import com.google.zxing.ResultMetadataType;
import com.google.zxing.ResultPoint;
import com.google.zxing.client.ohos.BeepManager;
import com.google.zxing.client.ohos.InactivityTimer;
import com.google.zxing.client.ohos.Intents;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;
import ohos.agp.utils.Rect;
import ohos.agp.window.dialog.CommonDialog;
import ohos.agp.window.dialog.IDialog;
import ohos.agp.window.service.Display;
import ohos.agp.window.service.DisplayManager;
import ohos.agp.window.service.Window;
import ohos.agp.window.service.WindowManager;
import ohos.bundle.AbilityInfo;
import ohos.bundle.IBundleManager;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.global.configuration.Configuration;
import ohos.global.resource.Element;
import ohos.global.resource.NotExistException;
import ohos.global.resource.ResourceManager;
import ohos.global.resource.WrongTypeException;
import ohos.media.image.ImagePacker;
import ohos.media.image.PixelMap;
import ohos.utils.PacMap;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Manages barcode scanning for a CaptureActivity. This class may be used to have a custom Ability
 * (e.g. with a customized look and feel, or a different superclass), but not the barcode scanning
 * process itself.
 *
 * This is intended for an Activity that is dedicated to capturing a single barcode and returning
 * it via setResult(). For other use cases, use DefaultBarcodeScannerView or BarcodeView directly.
 *
 * The following is managed by this class:
 * - Orientation lock
 * - InactivityTimer
 * - BeepManager
 * - Initializing from an Intent (via IntentIntegrator)
 * - Setting the result and finishing the Activity when a barcode is scanned
 * - Displaying camera errors
 */
public class CaptureManager {
    private static final String TAG = CaptureManager.class.getSimpleName();

    private static int cameraPermissionReqCode = 250;

    private Ability activity;
    private DecoratedBarcodeView barcodeView;
//    private int orientationLock = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
    private AbilityInfo.DisplayOrientation orientationLock = AbilityInfo.DisplayOrientation.UNSPECIFIED;
    private static final String SAVED_ORIENTATION_LOCK = "SAVED_ORIENTATION_LOCK";
    private boolean returnBarcodeImagePath = false;

    private boolean showDialogIfMissingCameraPermission = true;
    private String missingCameraPermissionDialogMessage = "";

    private boolean destroyed = false;

    private InactivityTimer inactivityTimer;
    private BeepManager beepManager;

    //private Handler handler;
    private EventHandler handler;

    private boolean finishWhenClosed = false;
    private Map<Integer,AbilityInfo.DisplayOrientation> orientationMap = new HashMap<Integer,AbilityInfo.DisplayOrientation>() {
        {
            put(0,AbilityInfo.DisplayOrientation.UNSPECIFIED);
            put(1,AbilityInfo.DisplayOrientation.LANDSCAPE);
            put(2,AbilityInfo.DisplayOrientation.PORTRAIT);
            put(3,AbilityInfo.DisplayOrientation.FOLLOWRECENT);
        }
    };

    private BarcodeCallback callback = new BarcodeCallback() {
        @Override
        public void barcodeResult(final BarcodeResult result) {
            barcodeView.pause();
            beepManager.playBeepSoundAndVibrate();

            handler.postTask(() -> returnResult(result));
        }

        @Override
        public void possibleResultPoints(List<ResultPoint> resultPoints) {

        }
    };

    private final CameraPreview.StateListener stateListener = new CameraPreview.StateListener() {
        @Override
        public void previewSized() {

        }

        @Override
        public void previewStarted() {

        }

        @Override
        public void previewStopped() {

        }

        @Override
        public void cameraError(Exception error) {
            try {
                displayFrameworkBugMessageAndExit(
                        activity.getResourceManager().getElement(ResourceTable.String_zxing_msg_camera_framework_bug).getString()
                );
            } catch (IOException | NotExistException | WrongTypeException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void cameraClosed() {
            if (finishWhenClosed) {
                finish();
            }
        }
    };

    public CaptureManager(Ability activity, DecoratedBarcodeView barcodeView) {
        this.activity = activity;
        this.barcodeView = barcodeView;
        barcodeView.getBarcodeView().addStateListener(stateListener);

        handler = new EventHandler(EventRunner.getMainEventRunner());

        inactivityTimer = new InactivityTimer(activity, () -> {
            finish();
        });

        beepManager = new BeepManager(activity);
    }

    /**
     * Perform initialization, according to preferences set in the intent.
     *
     * @param intent the intent containing the scanning preferences
     * @param savedInstanceState saved state, containing orientation lock
     */
    public void initializeFromIntent(Intent intent, PacMap savedInstanceState) {
        Window window = activity.getWindow();
        window.addFlags(WindowManager.LayoutConfig.MARK_SCREEN_ON_ALWAYS);

        if (savedInstanceState != null) {
            // If the screen was locked and unlocked again, we may start in a different orientation
            // (even one not allowed by the manifest). In this case we restore the orientation we were
            // previously locked to.
            int orientation = savedInstanceState.getIntValue(SAVED_ORIENTATION_LOCK, 0);
            this.orientationLock = orientationMap.get(orientation);
        }

        if (intent != null) {
            // Only lock the orientation if it's not locked to something else yet
            boolean orientationLocked = intent.getBooleanParam(Intents.Scan.ORIENTATION_LOCKED, true);
            if (orientationLocked) {
                lockOrientation();
            }

            if (Intents.Scan.ACTION.equals(intent.getAction())) {
                barcodeView.initializeFromIntent(intent);
            }

            if (!intent.getBooleanParam(Intents.Scan.BEEP_ENABLED, true)) {
                beepManager.setBeepEnabled(false);
            }

            if (intent.hasParameter(Intents.Scan.SHOW_MISSING_CAMERA_PERMISSION_DIALOG)) {
                setShowMissingCameraPermissionDialog(
                        intent.getBooleanParam(Intents.Scan.SHOW_MISSING_CAMERA_PERMISSION_DIALOG, true),
                        intent.getStringParam(Intents.Scan.MISSING_CAMERA_PERMISSION_DIALOG_MESSAGE)
                );
            }

            if (intent.hasParameter(Intents.Scan.TIMEOUT)) {
                handler.postTask(this::returnResultTimeout, intent.getLongParam(Intents.Scan.TIMEOUT, 0L));
            }

            if (intent.getBooleanParam(Intents.Scan.BARCODE_IMAGE_ENABLED, false)) {
                returnBarcodeImagePath = true;
            }
        }
    }

    /**
     * Lock display to current orientation.
     */
    protected void lockOrientation() {
        // Only get the orientation if it's not locked to one yet.
        if (this.orientationLock == AbilityInfo.DisplayOrientation.UNSPECIFIED) {
            // Adapted from http://stackoverflow.com/a/14565436
            DisplayManager displayManager = DisplayManager.getInstance();
            Display display = displayManager.getDefaultDisplay(activity).get();
            int rotation = display.getRotation();
//            int baseOrientation = activity.getResources().getConfiguration().orientation;
            int baseOrientation = activity.getResourceManager().getConfiguration().direction;

            AbilityInfo.DisplayOrientation orientation = AbilityInfo.DisplayOrientation.UNSPECIFIED;
            if (baseOrientation == Configuration.DIRECTION_HORIZONTAL) {

                if (rotation == Rect.RotationEnum.ROTATE_360.ordinal() || rotation == Rect.RotationEnum.ROTATE_90.ordinal()) {
                    orientation = AbilityInfo.DisplayOrientation.LANDSCAPE;
                } else {
                    // todo 纵向反方向，鸿蒙中无对应变量
//                    orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
                    orientation = AbilityInfo.DisplayOrientation.LANDSCAPE;
                }
            } else if (baseOrientation == Configuration.DIRECTION_VERTICAL) {
                if (rotation == Rect.RotationEnum.ROTATE_360.ordinal() || rotation == Rect.RotationEnum.ROTATE_270.ordinal()) {
                    orientation = AbilityInfo.DisplayOrientation.PORTRAIT;
                } else {
                    // todo 横向反方向，鸿蒙中无对应变量
//                    orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
                    orientation = AbilityInfo.DisplayOrientation.PORTRAIT;
                }
            }

            this.orientationLock = orientation;
        }

        activity.setDisplayOrientation(this.orientationLock);
    }

    /**
     * Start decoding.
     */
    public void decode() {
        barcodeView.decodeSingle(callback);
    }

    /**
     * Call from Activity#onResume().
     */
    public void onResume() {
        openCameraWithPermission();
        inactivityTimer.start();
    }

    private boolean askedPermission = false;

    private void openCameraWithPermission() {

        if (activity.verifySelfPermission("ohos.permission.CAMERA") != IBundleManager.PERMISSION_GRANTED) {

            if (activity.canRequestPermission("ohos.permission.CAMERA")) {

                activity.requestPermissionsFromUser(
                        new String[] { "ohos.permission.CAMERA" } , cameraPermissionReqCode);
                askedPermission = true;
            } else {

            }
        } else {
            barcodeView.resume();
        }
    }


    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode == cameraPermissionReqCode) {
            if (grantResults.length > 0 && grantResults[0] == IBundleManager.PERMISSION_GRANTED) {
                // permission was granted
                barcodeView.resume();
            } else {
                setMissingCameraPermissionResult();

                if (showDialogIfMissingCameraPermission) {
                    displayFrameworkBugMessageAndExit(missingCameraPermissionDialogMessage);
                } else {
                    closeAndFinish();
                }
            }
        }
    }

    /**
     * Call from Ability#onPause().
     */
    public void onPause() {

        inactivityTimer.cancel();
        barcodeView.pauseAndWait();
    }

    /**
     * Call from Ability#onDestroy().
     */
    public void onDestroy() {
        destroyed = true;
        inactivityTimer.cancel();
        //handler.removeCallbacksAndMessages(null);
        handler.removeAllEvent();
    }

    /**
     * Call from Ability#onSaveInstanceState().
     */
    public void onSaveInstanceState(PacMap outState) {
        outState.putIntValue(SAVED_ORIENTATION_LOCK, this.orientationLock.ordinal());
    }

    /**
     * Create a intent to return as the Ability result.
     *
     * @param rawResult the BarcodeResult, must not be null.
     * @param barcodeImagePath a path to an exported file of the Barcode Image, can be null.
     * @return the Intent
     */
    public static Intent resultIntent(BarcodeResult rawResult, String barcodeImagePath) {
        Intent intent = new Intent();
        intent.setAction(Intents.Scan.ACTION);
        intent.setParam(Intents.Scan.RESULT, rawResult.toString());
        intent.setParam(Intents.Scan.RESULT_FORMAT, rawResult.getBarcodeFormat().toString());
        byte[] rawBytes = rawResult.getRawBytes();
        if (rawBytes != null && rawBytes.length > 0) {
            intent.setParam(Intents.Scan.RESULT_BYTES, rawBytes);
        }
        Map<ResultMetadataType, ?> metadata = rawResult.getResultMetadata();
        if (metadata != null) {
            if (metadata.containsKey(ResultMetadataType.UPC_EAN_EXTENSION)) {
                intent.setParam(Intents.Scan.RESULT_UPC_EAN_EXTENSION,
                        metadata.get(ResultMetadataType.UPC_EAN_EXTENSION).toString());
            }
            Number orientation = (Number) metadata.get(ResultMetadataType.ORIENTATION);
            if (orientation != null) {
                intent.setParam(Intents.Scan.RESULT_ORIENTATION, orientation.intValue());
            }
            String ecLevel = (String) metadata.get(ResultMetadataType.ERROR_CORRECTION_LEVEL);
            if (ecLevel != null) {
                intent.setParam(Intents.Scan.RESULT_ERROR_CORRECTION_LEVEL, ecLevel);
            }
            @SuppressWarnings("unchecked")
            Iterable<byte[]> byteSegments = (Iterable<byte[]>) metadata.get(ResultMetadataType.BYTE_SEGMENTS);
            if (byteSegments != null) {
                int i = 0;
                for (byte[] byteSegment : byteSegments) {
                    intent.setParam(Intents.Scan.RESULT_BYTE_SEGMENTS_PREFIX + i, byteSegment);
                    i++;
                }
            }
        }
        if (barcodeImagePath != null) {
            intent.setParam(Intents.Scan.RESULT_BARCODE_IMAGE_PATH, barcodeImagePath);
        }
        return intent;
    }

    /**
     * Save the barcode image to a temporary file stored in the application's cache, and return its path.
     * Only does so if returnBarcodeImagePath is enabled.
     *
     * @param rawResult the BarcodeResult, must not be null
     * @return the path or null
     */
    private String getBarcodeImagePath(BarcodeResult rawResult) {
        String barcodeImagePath = null;
        if (returnBarcodeImagePath) {
//            Bitmap bmp = rawResult.getBitmap();
            PixelMap pixelMap = rawResult.getBitmap();
            try {
                File bitmapFile = File.createTempFile("barcodeimage", ".jpg", activity.getCacheDir());
                FileOutputStream outputStream = new FileOutputStream(bitmapFile);
                ImagePacker imagePacker = ImagePacker.create();
                ImagePacker.PackingOptions packingOptions = new ImagePacker.PackingOptions();
                packingOptions.quality = 100;
                packingOptions.format = "image/jpeg";
                imagePacker.initializePacking(outputStream,packingOptions);
                imagePacker.addImage(pixelMap);
                imagePacker.finalizePacking();
                outputStream.close();
                barcodeImagePath = bitmapFile.getCanonicalPath();
            } catch (IOException e) {
                Logger.getLogger(TAG).log(Level.WARNING, "Unable to create temporary file and store bitmap! " + e);
            }
        }
        return barcodeImagePath;
    }

    private void finish() {
        activity.terminateAbility();
    }

    protected void closeAndFinish() {
        if (barcodeView.getBarcodeView().isCameraClosed()) {
            finish();
        } else {
            finishWhenClosed = true;
        }

        barcodeView.pause();
        inactivityTimer.cancel();
    }

    private void setMissingCameraPermissionResult() {
        Intent intent = new Intent();
        intent.setAction(Intents.Scan.ACTION);
        intent.setParam(Intents.Scan.MISSING_CAMERA_PERMISSION, true);
        activity.setResult(0, intent);
    }

    protected void returnResultTimeout() {
        Intent intent = new Intent();
        intent.setAction(Intents.Scan.ACTION);
        intent.setParam(Intents.Scan.TIMEOUT, true);
        activity.setResult(0, intent);
        closeAndFinish();
    }

    protected void returnResult(BarcodeResult rawResult) {
        Intent intent = resultIntent(rawResult, getBarcodeImagePath(rawResult));
        activity.setResult(-1, intent);
        closeAndFinish();
    }

    protected void displayFrameworkBugMessageAndExit(String message) {
        if (activity.isTerminating() || this.destroyed || finishWhenClosed) {
            return;
        }

        try {
            if (message.isEmpty()) {
                message = activity.getResourceManager().getElement(ResourceTable.String_zxing_msg_camera_framework_bug).getString();
            }

            CommonDialog commonDialog = new CommonDialog(activity);
            ResourceManager resourceManager = activity.getResourceManager();
            Element element = resourceManager.getElement(ResourceTable.String_zxing_app_name);
            String string = element.getString();
            commonDialog.setTitleText(string);
            commonDialog.setContentText(message);
            commonDialog.setButton(0, activity.getResourceManager().getElement(ResourceTable.String_zxing_button_ok).getString(), new IDialog.ClickedListener() {
                @Override
                public void onClick(IDialog iDialog, int i) {
                    finish();
                }
            });
            commonDialog.setDestroyedListener(new CommonDialog.DestroyedListener() {
                @Override
                public void onDestroy() {
                    finish();
                }
            });
            commonDialog.show();
        } catch (IOException | WrongTypeException | NotExistException e) {
            e.printStackTrace();
        }
    }

    public static int getCameraPermissionReqCode() {
        return cameraPermissionReqCode;
    }

    public static void setCameraPermissionReqCode(int cameraPermissionReqCode) {
        CaptureManager.cameraPermissionReqCode = cameraPermissionReqCode;
    }

    /**
     * If set to true, shows the default error dialog if camera permission is missing.
     * <p>
     * If set to false, instead the capture manager just finishes.
     * <p>
     * In both cases, the ability result is set to {@link Intents.Scan#MISSING_CAMERA_PERMISSION}
     * and cancelled
     */
    public void setShowMissingCameraPermissionDialog(boolean visible) {
        setShowMissingCameraPermissionDialog(visible, "");
    }

    /**
     * If set to true, shows the specified error dialog message if camera permission is missing.
     * <p>
     * If set to false, instead the capture manager just finishes.
     * <p>
     * In both cases, the ability result is set to {@link Intents.Scan#MISSING_CAMERA_PERMISSION}
     * and cancelled
     */
    public void setShowMissingCameraPermissionDialog(boolean visible, String message) {
        showDialogIfMissingCameraPermission = visible;
        missingCameraPermissionDialogMessage = message != null ? message : "";
    }
}
